                                                  #include <stdio.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include <signal.h>
#include <errno.h>
#include <fcntl.h>

enum
{
    CMD_SOCKET_REGSITER = 1, // 注册
    CMD_SOCKET_LOGIN,        // 登录

    CMD_SOCKET_ADDFRIEND,  // 添加好友
    CMD_SOCKET_DELFRIEND,  // 删除好友
    CMD_SOCKET_CHATGPT,    // 聊天
    CMD_SOCKET_EXIT,       // 退出
    CMD_SOCKET_SHOWFRIEND, /*遍历好友*/

};

enum ReplyState
{
    CMD_SOCKET_REPLY_OK = 100, // 应答成功
    CMD_SOCKETT_REPLY_FAIL     // 应答失败
};

// 网络 数据结构体
typedef struct
{
    int cmd;                  // 命令控制位  enum SocketCmd
    int replyState;           // 应答标志位   enum ReplyState
    char replyErrorCode[256]; // 如果应答失败，则把错误的原因放在这里
    char account[256];        // 账号
    char passwd[256];         // 密码

    struct sockaddr_in addr; // 存储IP地址和端口号

    union
    {
        char msg[1024]; // 聊天的数据

    } cmdData;

    //......
} SockData_t;
typedef struct
{
    char account[256]; // 好友的账号
    char msg[1024];
    struct sockaddr_in addr; // 好友的IP地址和端口号  --直接以  网络字节序的方式存在

} NodeData_t;

// 客户端的好友列表 使用链表去存储
typedef struct node
{
    NodeData_t data;
    struct node *next;
} Node_t;

struct list
{
    Node_t *head;
    Node_t *tail;

    int nodeNumber;
};

#define SERVER_ADDR "127.0.0.1 "
#define SERVER_PORT 12300

int g_tcpSocketFd;
int g_udpSocketFd;
struct list *list;

int initList()
{
    // 申请 链表的管理结构体内存空间
    list = malloc(sizeof(struct list));
    if (list == NULL)
    {
        perror("malloc list error");
        return -1;
    }

    list->head = list->tail = NULL;
    list->nodeNumber = 0;

    return 0;
}

int insertNodeToList(NodeData_t inputData)
{
    // 1、先判断 头节点不为NULL
    if (list == NULL)
    {
        return -1;
    }
    // 2、申请新数据结点内存空间并且初始化
    Node_t *newNode = malloc(sizeof(Node_t));
    if (newNode == NULL)
    {
        perror("malloc newNode error");
        return -1;
    }
    newNode->data = inputData;
    newNode->next = NULL;

    // 3、判断当前链表的数据结点是为 0
    if (list->nodeNumber == 0)
    {
        // 头节点的head 和 tail  都指向 这一个数据结点
        list->head = newNode;
        list->tail = newNode;
    }
    else
    {

        list->tail->next = newNode;

        // 更新尾结点
        list->tail = newNode;
    }

    // 4、结点数量+1
    list->nodeNumber++;
}

/*-----------------注册-------------------*/

int loginWidget_register()
{
    SockData_t data;
err:
    // 1、通过手动输入注册的账号 和 密码
    memset(&data, 0, sizeof(data));

    printf("请输入账号:");
    scanf("%s", data.account);

    printf("请输入密码:");
    scanf("%s", data.passwd);

    // 2、组合网络数据包（把账号和密码 封装成 一个数据包）
    data.cmd = CMD_SOCKET_REGSITER;

    // 3、通过TCP协议 把 数据包  发送给服务器
    int w_byte = write(g_tcpSocketFd, &data, sizeof(SockData_t));
    if (w_byte != sizeof(SockData_t))
    {
        printf("\n发送 数据包 失败，注册失败...\n");
        goto err;
    }
    // 4、阻塞等待 服务器进行ACK 应答
    memset(&data, 0, sizeof(data)); // 清空，然后存储 服务器 应答的数据

    read(g_tcpSocketFd, &data, sizeof(data));

    // 5、解析应答包，判断刚才的注册是否成功
    if (data.replyState == CMD_SOCKET_REPLY_OK)
    {
        printf("register success\n");
        return true;
    }
    else
    {
        printf("\nregister fail:%s\n", data.replyErrorCode);
        return false;
    };
}

/*-----------------登录-------------------*/

int loginWidget_login()
{
    SockData_t data;
err:
    memset(&data, 0, sizeof(data));

    // 1、通过手动输入登录的账号 和 密码
    printf("请输入账号:");
    scanf("%s", data.account);

    printf("请输入密码:");
    scanf("%s", data.passwd);

    // 2、组合网络数据包（把账号和密码 封装成 一个数据包）
    data.cmd = CMD_SOCKET_LOGIN;

    // 3、通过TCP协议 把 数据包  发送给服务器
    int w_byte = write(g_tcpSocketFd, &data, sizeof(SockData_t));
    if (w_byte != sizeof(SockData_t))
    {
        printf("\n发送 数据包 失败，登录失败...\n");
        goto err;
    }

    // 4、阻塞等待 服务器进行ACK 应答
    memset(&data, 0, sizeof(data)); // 清空，然后存储 服务器 应答的数据

    read(g_tcpSocketFd, &data, sizeof(data));

    // 5、解析应答包，判断刚才的登录是否成功
    if (data.replyState == CMD_SOCKET_REPLY_OK)
    {
        printf("登录成功......\n");
        return true;
    }
    else
    {
        printf("\n登录失败:%s\n", data.replyErrorCode);
        return false;
    }
}

/*-----------------登录、注册-------------------*/

int loginWidget()
{
    int mode;
    bool okFlag = false;

    while (1)
    {
        printf("\n[1]注册  [2]登录:");
        scanf("%d", &mode);

        switch (mode)
        {
        case CMD_SOCKET_REGSITER:
            loginWidget_register();
            break;
        case CMD_SOCKET_LOGIN:
            okFlag = loginWidget_login();
            break;
        default:
            break;
        }

        if (okFlag)
            break;
    }

    return okFlag;
}

/*-----------------添加好友-------------------*/

void mainWidget_addFriend()
{
    // 1、手动输入你要添加的好友用户名（账号）
    SockData_t data;
    memset(&data, 0, sizeof(data));

    printf("请输入添加的好友账号:");
    scanf("%s", data.account);

    // 2、遍历链表，查看当前你要添加的好友 是否 已经 是你的好友了
    Node_t *p = list->head;
    while (p)
    {
        if (strcmp(p->data.account, data.account) == 0)
        {
            // 说明已经是 好友了，没必要再添加
            printf("该用户[%s] 已经是好友了.....\n", data.account);
            return;
        }

        p = p->next;
    }

    // 3、组合 网络包数据
    data.cmd = CMD_SOCKET_ADDFRIEND;

    // 4、把数据以Tcp形式发送给 服务器
    int w_byte = write(g_tcpSocketFd, &data, sizeof(SockData_t));
    if (w_byte != sizeof(SockData_t))
    {
        printf("发送 数据包 失败  添加好友失败...\n");
    }

    // 5、等待 服务器进行应答
    printf("等待服务器 应答......\n");
    memset(&data, 0, sizeof(data)); // 清空，然后存储 服务器 应答的数据

    int ret = read(g_tcpSocketFd, &data, sizeof(data));
    if (ret > 0)
    {
        printf("服务器 应答成功......\n");
    }
    else
    {
        printf("服务器 应答失败.....\n");
    }

    // 6、根据应答包 获取 是否 添加成功
    if (data.replyState == CMD_SOCKET_REPLY_OK)
    {
        // 7、如果添加成功了，那么 使用尾插法 将当前好友的用户名、IP地址和端口号  插入到链表中
        NodeData_t inputData;
        memset(&inputData, 0, sizeof(inputData));

        strcpy(inputData.account, data.account); // 好友的用户名
        inputData.addr = data.addr;              // 好友的IP地址和端口号
        insertNodeToList(inputData);

        printf("添加好友成功....\n");
    }
    else
    {
        printf("添加好友失败:%s\n", data.replyErrorCode);
    }
}
/*-----------------遍历好友-------------------*/
void TraverseNodes()
{
    // 1.指向链表中的第一个节点
    Node_t *p = list->head;

    if (p != NULL)
    {
        printf("account:%s\n", p->data.account);

        p = p->next;
    }
    else
    {
        printf("暂无更多好友！\n");
    }
}

/*-----------------删除好友-------------------*/

void zmainWidget_delFriend()
{
    // 1、手动输入你要删除的好友用户名（账号）
    SockData_t data;
    memset(&data, 0, sizeof(data));

    printf("请输入删除的好友账号:");
    scanf("%s", data.account);

    // 2、遍历链表，找到当前你要删除的好友 是否 不 是你的好友了
    Node_t *p = list->head;
    Node_t *pre = NULL;

    while (p)
    {
        if (strcmp(p->data.account, data.account) == 0)
        {
            // 找到当前你要删除的好友
            printf("找到要删除的用户[%s] .....\n", data.account);
            break;
        }

        pre = p;
        p = p->next;
    }

    if (p == NULL) // 没找到当前你要删除的好友
    {
        printf("找不到要删除的用户[%s] .....\n", data.account);
        return;
    }

    // 3、组合 网络包数据
    data.cmd = CMD_SOCKET_DELFRIEND;

    // 4、把数据包 发送给 服务器
    int w_byte = write(g_tcpSocketFd, &data, sizeof(SockData_t));
    if (w_byte != sizeof(SockData_t))
    {
        printf("发送 数据包 失败  删除好友失败...\n");
    }

    // 5、等待 服务器进行应答
    printf("\n等待服务器 应答......\n");
    memset(&data, 0, sizeof(data)); // 清空，然后存储 服务器 应答的数据

    int ret = read(g_tcpSocketFd, &data, sizeof(data));
    if (ret > 0)
    {
        printf("\n服务器 应答成功......\n");
    }
    else
    {
        printf("\n服务器 应答失败.....\n");
    }

    // 6、根据应答包 获取 是否 删除成功
    if (data.replyState == CMD_SOCKET_REPLY_OK)
    {
        // 3、如果找到，但是为首结点，且首结点有后继
        if (p == list->head)
        {
            list->head = p->next;
            p->next = NULL;
            free(p);
            else // 4、如果找到，但是并非为首结点，而是中间结点或者是尾结点
            {
                if (p == list->tail)
                {
                    list->tail = pre;
                }

                pre->next = p->next;
                p->next = NULL;
                free(p); // 释放需删除结点内存空间
            }

            list->nodeNumber--; // 跟新管理结构体的结点数量信息

            printf("\n删除好友成功....\n");
        }
        else
        {
            printf("\n添加好友失败:%s\n", data.replyErrorCode);
        }
    }
}
#if 1

/*-----------------聊天-------------------*/

int mainWidget_chatgpt()
{
    SockData_t data;
    memset(&data, 0, sizeof(data));

    // 1、手动输入 你要聊天的对象
    printf("请输入你要聊天的好友:\n");
    char name[256] = {0};
    scanf("%s", name);

    // 2、手动输入 聊天的数据
    printf("请输入你要发送的数据:\n");
    scanf("%s", data.cmdData.msg);

    // 3、查找你聊天的对象是不是你的好友
    Node_t *p = list->head;
    while (p)
    {
        // 找到了
        if (!strcmp(p->data.account, name))
        {
            break;
        }
        p = p->next;
    }

    if (p == NULL) // 表示没有找到
    {
        printf("\n对方不是你的好友，请先添加%s 为好友\n", name);
        return -1;
    }

    // 4、组合网络数据包
    data.cmd == CMD_SOCKET_CHATGPT;
    // 在data中 存储 你要发送好友的IP地址和端口号
    data.addr = p->data.addr;
    // 在data中 存储 你要发送好友的账号
    strcpy(data.account, p->data.account);

    // 5、发送给服务器 使用UDP协议的方式
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    serverAddr.sin_port = htons(SERVER_PORT);

    int ret = sendto(g_udpSocketFd, &data, sizeof(data), 0, (struct sockaddr *)&serverAddr, sizeof(serverAddr));

    printf("\n我要发给%s好友 发送数据:%s  好友的IP地址:%s 端口号:%d\n",
           name,
           data.cmdData.msg,
           inet_ntoa(data.addr.sin_addr),
           ntohs(data.addr.sin_port));
}

#endif

/*-----------------界面-------------------*/

void mainWidget()
{
    while (1)
    {
        int mode;
        bool exitFlag = false;

        printf("\n[3]添加好友  [4]删除好友  [5]聊天  [6]退出 [7]遍历好友:");
        scanf("%d", &mode);

        switch (mode)
        {
        case CMD_SOCKET_ADDFRIEND: // 添加好友
            mainWidget_addFriend();
            break;
        case CMD_SOCKET_SHOWFRIEND: // 遍历好友
            TraverseNodes();
            break;
        case CMD_SOCKET_DELFRIEND: // 删除好友
            zmainWidget_delFriend();
            break;
        case CMD_SOCKET_CHATGPT: // 聊天
            mainWidget_chatgpt();
            break;
        case CMD_SOCKET_EXIT: // 退出
            exitFlag = true;
            break;
        default:
            break;
        }

        if (exitFlag)
            break;
    }
}

/*-----------------初始化Tcp-------------------*/

int initTcp(int port)
{
    // 1、建立套接字
    g_tcpSocketFd = socket(AF_INET, SOCK_STREAM, 0);
    if (g_tcpSocketFd == -1)
    {
        perror("socket error");
        return -1;
    }

    // 2、绑定自己IP地址和端口号
    struct sockaddr_in ownAddr;
    ownAddr.sin_family = AF_INET;
    ownAddr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 代表本机所有的IP地址
    ownAddr.sin_port = htons(port);
    int ret = bind(g_tcpSocketFd, (struct sockaddr *)&ownAddr, sizeof(ownAddr));
    if (ret == -1)
    {
        perror("bind error");
        return -1;
    }

    // 2、发起连接
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(SERVER_ADDR);
    serverAddr.sin_port = htons(SERVER_PORT);

    ret = connect(g_tcpSocketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (ret == -1)
    {
        perror("connect error");
    }
    else
    {
        printf("connect success\n");
    }

    return 0;
}

/*-----------------Udp接收数据-------------------*/

void recvDataHandle(int num) /*信号响应*/
{
    SockData_t data;
    memset(&data, 0, sizeof(data));

    int ret = recvfrom(g_udpSocketFd, &data, sizeof(data), 0, NULL, NULL);

    printf("\nmsg:%s size:%d\n", data.cmdData.msg, ret);
}

/*-----------------初始化Ucp-------------------*/

int initUdp(int port)
{
    // 1、建立套接字
    g_udpSocketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (g_udpSocketFd == -1)
    {
        perror("socketFd error");
        return -1;
    }

    // 端口号服用
    int opt = 1;
    setsockopt(g_udpSocketFd, SOL_SOCKET, SO_REUSEADDR, &opt, (socklen_t)sizeof(opt));

    // 2、绑定自己的IP地址和端口号
    struct sockaddr_in serverAddr;
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(port);
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    int ret = bind(g_udpSocketFd, (struct sockaddr *)&serverAddr, sizeof(serverAddr));
    if (ret == -1)
    {
        perror("bind error");
        return -1;
    }

    // 3、设置信号的SIGIO的信号响应函数
    // 也就是说，当有客户端发送数据过来的时候，内核通过套接字会自动发射SIGIO信号
    signal(SIGIO, recvDataHandle);

    // 4、设置信号SIGIO的属主进程
    // 也就是说，当内核发射出这个SIGIO信号之后，设置哪个进程去响应它
    fcntl(g_udpSocketFd, F_SETOWN, getpid());

    // 5、默认情况下，客户端有数据发送过来的时候，内核不会自动发射SIGIO信号
    // 也就是说 这个模式是关闭的，那么现在要开启生效它，就是有数据发来的时候，内核自动发射该信号
    // 1)先获取原来套接字文件描述符的属性
    int status = fcntl(g_udpSocketFd, F_GETFL);
    // 2）将获取到的属性变量中 加入 信号触发模式功能
    status |= O_ASYNC;
    // 3)把修改好的属性变量 重新设置回套接字中
    fcntl(g_udpSocketFd, F_SETFL, status);
}

int main(int argc, char *argv[])
{
    if (argc < 2)
    {
        printf("输入客户端端口号");
        return -1;
    }

    printf("客户端上线...\n");

    initTcp(atoi(argv[1]));
    initUdp(atoi(argv[1]));
    initList();

    while (1)
    {
        loginWidget(); // 注册登录功能

        mainWidget(); // 主界面功能
    }

    return 0;
}
